#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#define BMP_IMPLEMENTATION
#include "bmp.h"
#define TLG_IMPLEMENTATION
#include "tlg.h"
#define STBI_IMPLEMENTATION
#include "stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
/*#define STBIW_ZLIB_COMPRESS compress2*/
#include "stb_image_write.h"
#define MSF_GIF_IMPL
#include "msf_gif.h"
/* AAAAArghhhhh fuck google big time, we can't include webp/decode.h it causes errors */
uint8_t* WebPDecodeRGBA(const uint8_t* data, size_t data_size, int* width, int* height);
/* And fuck ijg too, what kind of messed up api is this? */
#include <stdio.h>
#include <setjmp.h>
#include "libjpeg/jpeglib.h"

/* important to include this AFTER we have the implementations */
#include "main.h"

sprite_sheet_t *sheets = NULL;
int numsheets = 0;
sprite_t *sprites = NULL;
int numsprites = 0, insplit = 0;
static int cursprite = -1, curframe = 0, smooth = 1, splitcols = 2, splitrows = 2, frmpos = 0;

static jmp_buf jpeg_jmpbuf;
static void jpeg_silent(j_common_ptr cinfo) {
    jpeg_destroy(cinfo);
    longjmp(jpeg_jmpbuf, 1);
}

/**
 * Detect if it's an image asset that we know
 */
int sprites_detect(uint8_t *buf)
{
    return !memcmp(buf, "BM", 2) ||
        !memcmp(buf, "TLG", 3) || !memcmp(buf, "XXX", 3) || !memcmp(buf, "JKM", 3) ||
        !memcmp(buf, "\x89PNG", 4) ||
        !memcmp(buf, "GIF8", 4) ||
        (!memcmp(buf, "RIFF", 4) && !memcmp(buf + 8, "WEBP", 4)) ||
        (buf[0] == 0xff && buf[1] == 0xd8 && !memcmp(buf + 6, "JFIF", 4));
}

/**
 * Decode an image asset into a 32-bit RGBA pixel buffer
 */
uint32_t *sprites_decode(uint8_t *buf, uint64_t size, int *w, int *h)
{
    struct jpeg_decompress_struct cinfo;
    struct jpeg_error_mgr jerr;
    volatile JSAMPROW row = 0, currow;
    JSAMPROW rowptr[1];
    JDIMENSION i, j;
    stbi__context s;
    stbi__result_info ri;
    uint32_t *volatile ret = NULL;
    uint32_t *pix;
    int p;

    if(!buf || size < 16 || !w || !h) return NULL;
    *w = *h = 0;

    if(!memcmp(buf, "BM", 2)) {
        ret = bmp_load(buf, size, w, h);
        if(ret && verbose > 1) debug("bmp decode width %d height %d", *w, *h);
    } else
    if(!memcmp(buf, "TLG", 3) || !memcmp(buf, "XXX", 3) || !memcmp(buf, "JKM", 3)) {
        ret = tlg_load(buf, size, w, h);
        if(ret && verbose > 1) debug("tlg decode width %d height %d", *w, *h);
    } else
    if(!memcmp(buf, "\x89PNG", 4)) {
        s.read_from_callbacks = 0;
        s.img_buffer = s.img_buffer_original = buf;
        s.img_buffer_end = s.img_buffer_original_end = buf + size;
        ri.bits_per_channel = 8;
        ret = (uint32_t*)stbi__png_load(&s, w, h, &p, 4, &ri);
        if(ret && verbose > 1) debug("png decode width %d height %d", *w, *h);
    } else
    if(!memcmp(buf, "GIF8", 4)) {
        s.read_from_callbacks = 0;
        s.img_buffer = s.img_buffer_original = buf;
        s.img_buffer_end = s.img_buffer_original_end = buf + size;
        ri.bits_per_channel = 8;
        ret = (uint32_t*)stbi__gif_load(&s, w, h, &p, 4, &ri);
        if(ret && verbose > 1) debug("gif decode width %d height %d", *w, *h);
    } else
    if(!memcmp(buf, "RIFF", 4) && !memcmp(buf + 8, "WEBP", 4)) {
        ret = (uint32_t*)WebPDecodeRGBA((const uint8_t*)buf, (size_t)size, w, h);
        if(ret && verbose > 1) debug("webp decode width %d height %d", *w, *h);
    } else
    if(buf[0] == 0xff && buf[1] == 0xd8 && !memcmp(buf + 6, "JFIF", 4)) {
        memset(&cinfo, 0, sizeof(cinfo));
        memset(&jerr, 0, sizeof(jerr));
        cinfo.err=jpeg_std_error(&jerr);
        cinfo.err->output_message = jpeg_silent;
        cinfo.err->error_exit = jpeg_silent;
        if(setjmp(jpeg_jmpbuf) != 0) {
jpegerr:    jpeg_destroy_decompress(&cinfo);
            if(row) free(row);
            if(ret) free((uint32_t *)ret);
            *w = *h = 0;
            return NULL;
        }
        jpeg_create_decompress(&cinfo);
        jpeg_mem_src(&cinfo, buf, size);
        if(jpeg_read_header(&cinfo, 1) != JPEG_HEADER_OK) goto jpegerr;
        cinfo.out_color_space = JCS_RGB;
        jpeg_start_decompress(&cinfo);
        *w = cinfo.output_width;
        *h = cinfo.output_height;
        if(!(ret = (uint32_t*)malloc((*w) * (*h) * 4)) ||
           !(row = malloc(cinfo.output_width * cinfo.output_components * sizeof(JSAMPLE)))) goto jpegerr;
        rowptr[0] = row;
        pix = (uint32_t *)ret;
        for(i = 0; i < cinfo.output_height; i++) {
            currow = row;
            if(jpeg_read_scanlines(&cinfo, rowptr, 1) != 1) goto jpegerr;
            for(j = 0; j < cinfo.output_width; j++, currow += cinfo.output_components, pix++)
                if(cinfo.jpeg_color_space == JCS_GRAYSCALE)
                    *pix = ((*currow & 0xff) << 16) | ((*currow & 0xff) << 8) | (*currow & 0xff) | 0xff000000;
                else
                    *pix = (*currow) | 0xff000000;
        }
        free(row);
        jpeg_destroy_decompress(&cinfo);
        if(ret && verbose > 1) debug("jpeg decode width %d height %d", *w, *h);
    }
    return (uint32_t *)ret;
}

/**
 * Save a sprite into PNG (1 frame) or animated GIF (multiple frames)
 */
uint8_t *sprites_encode(int sprite, uint64_t *size)
{
    MsfGifState gifState;
    MsfGifResult result;
    uint8_t *ret = NULL;
    uint32_t *img;
    int i, s, d, w, h, n;

    if(size && sprite >= 0 && sprite < numsprites && sprites[sprite].frame && sprites[sprite].numframe > 0 &&
        sprites[sprite].w > 0 && sprites[sprite].h > 0 && (img = (uint32_t*)malloc(sprites[sprite].w * sprites[sprite].h * 4))) {
            w = sprites[sprite].w; h = sprites[sprite].h; n = sprites[sprite].numframe;
            if(n > 1) {
                msf_gif_bgra_flag = 0;
                msf_gif_alpha_threshold = 127;
                msf_gif_begin(&gifState, w, h);
            }
            for(i = 0; i < n; i++) {
                memset(img, 0, w * h * 4);
                sprites_draw(sprite, i, img, w, h, w, 0, 0, 1);
                if(n > 1) {
                    d = sprites[sprite].frame[i].dur / 10;
                    msf_gif_frame(&gifState, (uint8_t*)img, d < 10 ? 10 : d, 16, w * 4);
                }
            }
            if(n > 1) {
                result = msf_gif_end(&gifState);
                if(result.data) {
                    ret = result.data;
                    *size = result.dataSize;
                }
            } else {
                if((ret = stbi_write_png_to_mem((uint8_t*)img, w * 4, w, h, 4, &s))) {
                    if(sprites[sprite].frame[0].s >= 0 && sprites[sprite].frame[0].s < numsheets &&
                        sheets[sprites[sprite].frame[0].s].esiz && sheets[sprites[sprite].frame[0].s].extra &&
                        (ret = (uint8_t*)realloc(ret, s + sheets[sprites[sprite].frame[0].s].esiz))) {
                            memcpy(ret + s, sheets[sprites[sprite].frame[0].s].extra, sheets[sprites[sprite].frame[0].s].esiz);
                            s += sheets[sprites[sprite].frame[0].s].esiz;
                        }
                }
                *size = s;
            }
            free(img);
    }
    if(!ret) *size = 0;
    return ret;
}

/**
 * Free sprite list, but keep the sprite sheets in memory
 */
void sprites_freesprites(void)
{
    int i;
    if(sprites) {
        for(i = 0; i < numsprites; i++) {
            if(sprites[i].frame) free(sprites[i].frame);
        }
        free(sprites); sprites = NULL;
    }
    numsprites = 0; cursprite = -1;
}

/**
 * Free all lists
 */
void sprites_free(void)
{
    int i;
    if(sheets) {
        for(i = 0; i < numsheets; i++) {
            if(sheets[i].data) free(sheets[i].data);
            if(sheets[i].path) free(sheets[i].path);
            if(sheets[i].extra) free(sheets[i].extra);
        }
        free(sheets); sheets = NULL;
    }
    numsheets = 0;
    sprites_freesprites();
}

/**
 * Add a new sprite sheet from an image asset
 */
int sprites_addsheet(int id, char *name, uint8_t *buf, uint32_t size)
{
    uint32_t *data;
    int i, w, h;

    if(sheets) {
        if(id != -1) {
            for(i = 0; i < numsheets; i++)
                if(sheets[i].id == id) return i;
        } else
        if(name && *name)
            for(i = 0; i < numsheets; i++)
                if(!strcmp(sheets[i].name, name)) return i;
    }
    if(buf && size) {
        if((data = sprites_decode(buf, size, &w, &h))) {
            i = numsheets++;
            if((sheets = (sprite_sheet_t*)realloc(sheets, numsheets * sizeof(sprite_sheet_t)))) {
                engine_ram += sizeof(sprite_sheet_t);
                memset(&sheets[i], 0, sizeof(sprite_sheet_t));
                sheets[i].id = id;
                if(name && *name) strncpy(sheets[i].name, name, NAMELEN - 1);
                sheets[i].w = w;
                sheets[i].h = h;
                sheets[i].data = data;
                return i;
            } else {
                engine_features |= EF_NOMEM;
                debug("memory allocation error, too many sprite sheets %u", numsheets);
                numsheets = 0;
                free(data);
            }
        }
    }
    return -1;
}

/**
 * Create a new empty sprite sheet
 */
int sprites_newsheet(int id, char *name, int w, int h)
{
    int i;
    if(sheets) {
        if(id != -1) {
            for(i = 0; i < numsheets; i++)
                if(sheets[i].id == id) return i;
        } else
        if(name && *name)
            for(i = 0; i < numsheets; i++)
                if(!strcmp(sheets[i].name, name)) return i;
    }
    if(w > 0 && h > 0) {
        i = numsheets++;
        if((sheets = (sprite_sheet_t*)realloc(sheets, numsheets * sizeof(sprite_sheet_t)))) {
            engine_ram += sizeof(sprite_sheet_t);
            memset(&sheets[i], 0, sizeof(sprite_sheet_t));
            sheets[i].id = id;
            if(name && *name) strncpy(sheets[i].name, name, NAMELEN - 1);
            sheets[i].w = w;
            sheets[i].h = h;
            if((sheets[i].data = (uint32_t*)malloc(w * h * 4))) {
                memset(sheets[i].data, 0, w * h * 4);
                return i;
            } else {
                engine_features |= EF_NOMEM;
                debug("memory allocation error, sprite sheet too big %u x %u", w, h);
                numsheets--;
            }
        } else {
            engine_features |= EF_NOMEM;
            debug("memory allocation error, too many sprite sheets %u", numsheets);
            numsheets = 0;
        }
    }
    return -1;
}

/**
 * Merge multiple sprites into one animated sprite with multiple frames
 */
void sprites_merge(int from, int to)
{
    sprite_frame_t *frame;
    int i, l, s, w, h;

    if(from < 0 || from >= numsprites || to < 0 || to >= numsprites || to == from || !sprites[from].frame || !sprites[to].frame)
        return;
    if(to < from) { i = to; to = from; from = i; }
    for(i = from, w = h = l = 0; i <= to; i++) {
        if(sprites[i].w > w) w = sprites[i].w;
        if(sprites[i].h > h) h = sprites[i].h;
        l += sprites[i].numframe;
    }
    i = from;
    if((s = sprites_add(-1, NULL, w, h, 0, 0, sprites[i].frame[0].s, sprites[i].frame[0].x, sprites[i].frame[0].y,
        sprites[i].frame[0].w, sprites[i].frame[0].h, sprites[i].frame[0].dur, sprites[i].frame[0].sfx)) != -1) {
        sprintf(sprites[s].name, "sprite%05u", s);
        if(!(sprites[s].frame = frame = (sprite_frame_t*)realloc(sprites[s].frame, l * sizeof(sprite_frame_t)))) {
            engine_features |= EF_NOMEM;
            debug("memory allocation error, too many frames for sprite #%u (id %u)", s, sprites[s].id);
            sprites[s].numframe = 0;
            return;
        }
        for(i = from, sprites[s].numframe = 0; i <= to; i++)
            if(sprites[i].frame) {
                memcpy(frame, sprites[i].frame, sprites[i].numframe * sizeof(sprite_frame_t));
                sprites[s].numframe += sprites[i].numframe;
                frame += sprites[i].numframe;
            }
        sprites_select(s);
    }
}

/**
 * Split one sprite into an animated sprite with multiple frames
 */
void sprites_split(int sprite, int cols, int rows)
{
    sprite_sheet_t *sheet;
    sprite_frame_t *frame;
    uint32_t *pix;
    int i, j, s, x, y, w, h;

    insplit = 0;
    if(sprite < 0 || sprite >= numsprites || (cols == 1 && rows == 1) || cols < 1 || cols > sprites[sprite].w / 2 ||
        rows < 1 || rows > sprites[sprite].h / 2 || !sprites[sprite].frame || sprites[sprite].numframe != 1 ||
        sprites[sprite].frame[0].s < 0 || sprites[sprite].frame[0].s > numsheets || !sheets[sprites[sprite].frame[0].s].data)
        return;
    if(!(sprites[sprite].frame = frame = (sprite_frame_t*)realloc(sprites[sprite].frame, cols * rows * sizeof(sprite_frame_t)))) {
        engine_features |= EF_NOMEM;
        debug("memory allocation error, too many frames for sprite #%u (id %u)", sprite, sprites[sprite].id);
        sprites[sprite].numframe = 0;
        return;
    }
    s = sprites[sprite].frame[0].s;
    sheet = &sheets[s];
    sprites[sprite].w = w = sheet->w / cols;
    sprites[sprite].h = h = sheet->h / rows;
    memset(frame, 0, cols * rows * sizeof(sprite_frame_t));
    for(i = sprites[sprite].numframe = 0; i < cols * rows; i++) {
        frame->s = s;
        frame->x = (i % cols) * w;
        frame->y = (i / cols) * h;
        frame->w = w;
        frame->h = h;
        frame->dur = 100;
        frame->sfx = -1;
        for(y = j = 0, pix = sheet->data + frame->y * sheet->w + frame->x; !j && y < h; y++, pix += sheet->w)
            for(x = 0; !j && x < w; x++)
                if(pix[x] & 0xff000000) j = 1;
        if(j) {
            frame++;
            sprites[sprite].numframe++;
        }
    }
    if(sprites[sprite].numframe) {
        w = 64; h = 64 * sprites[sprite].h / sprites[sprite].w;
        if(h > 64) { h = 64; w = 64 * sprites[sprite].w / sprites[sprite].h; }
        memset(sprites[sprite].preview, 0, 64 * 64 * 4);
        resize(sprites[sprite].preview, (64 - w) / 2, (64 - h) / 2, w, h, 64,
            (sheet->data + (sprites[sprite].frame[0].y * sheet->w + sprites[sprite].frame[0].x)),
                sprites[sprite].w, sprites[sprite].h, sheet->w);
    }
}

/**
 * Replace an animated sprite with multiple frame with one frame that covers the entire sprite sheet
 */
void sprites_unsplit(int sprite)
{
    int w, h;

    insplit = 0;
    if(sprite < 0 || sprite >= numsprites || !sprites[sprite].frame || sprites[sprite].numframe == 1 ||
        sprites[sprite].frame[0].s < 0 || sprites[sprite].frame[0].s > numsheets || !sheets[sprites[sprite].frame[0].s].data)
        return;
    sprites[sprite].w = sprites[sprite].frame[0].w = sheets[sprites[sprite].frame[0].s].w;
    sprites[sprite].h = sprites[sprite].frame[0].h = sheets[sprites[sprite].frame[0].s].h;
    sprites[sprite].frame[0].x = sprites[sprite].frame[0].y = 0;
    sprites[sprite].numframe = 1;
    w = 64; h = 64 * sprites[sprite].h / sprites[sprite].w;
    if(h > 64) { h = 64; w = 64 * sprites[sprite].w / sprites[sprite].h; }
    memset(sprites[sprite].preview, 0, 64 * 64 * 4);
    resize(sprites[sprite].preview, (64 - w) / 2, (64 - h) / 2, w, h, 64,
        sheets[sprites[sprite].frame[0].s].data, sprites[sprite].w, sprites[sprite].h, sprites[sprite].w);
}

/**
 * Detect the most likely sprite grid on a sprite sheet
 */
void sprites_colrow(int sheet, int *cols, int *rows)
{
    int s, k, l, m, n, o, p, q;

    if(!cols || !rows) return;
    *cols = *rows = 1;
    if(sheet < 0 || sheet >= numsheets || !sheets[sheet].data || sheets[sheet].w < 16 || sheets[sheet].h < 16) return;

    /* this is not the best algorithm, only works surely if sprites are separated by an empty (transparent) line and column.
     * how it works (for rows, same goes for columns):
     * 1. set best match to 50% (o, meaning every scanline must be at least 50% empty to be considered)
     * 2. try various values from 1/16 to half of height (n in pixels, p as a divider, only if height is multiple of n)
     * 3. for each scanline at every multiple of given height count the percentage of transparent pixels (m)
     * 4. if the percentage is better than the previous best, consider this a better match
     * (instead of an actual percentage we use 50 and m - 1 to counteract rounding errors)
     */
    /* detect columns on sheet */
    for(o = 25, n = sheets[sheet].w / 16; n <= sheets[sheet].w / 2; n++) {
        p = sheets[sheet].w / n;
        if(p > 1 && p * n == sheets[sheet].w) {
            s = ((p - 1) * sheets[sheet].h);
            for(k = n, m = 0; k < sheets[sheet].w; k += n)
                for(l = 0; l < sheets[sheet].h; l++)
                    if((sheets[sheet].data[l * sheets[sheet].w + k] >> 24) < 4) m++;
            m = m * 50 / s;
            if(m - 1 > o) { o = m; *cols = p; }
        }
    }
    /* detect rows on sheet */
    q = 2 * sheets[sheet].w / *cols / 3;
    for(o = 25, n = sheets[sheet].h / 16; n <= sheets[sheet].h / 2; n++) {
        p = sheets[sheet].h / n;
        if(p > 1 && p * n == sheets[sheet].h) {
            s = ((p - 1) * sheets[sheet].w);
            for(k = n, m = 0; k < sheets[sheet].h; k += n)
                for(l = 0; l < sheets[sheet].w; l++)
                    if((sheets[sheet].data[k * sheets[sheet].w + l] >> 24) < 4) m++;
            m = m * 50 / s;
            /* little tweaking: below 3:2 ratio, at least 99% of pixels must be transparent */
            if(m - 1 > o && (n >= q || m >= 49)) { o = m; *rows = p; }
        }
    }
}

/**
 * Detect sprites on sheets and create animated sprites if possible
 */
void sprites_scansheets(void)
{
    int i, j, cols, rows;

    if(sheets && numsheets) {
        sprites_freesprites();
        for(i = 0; i < numsheets; i++) {
            status(SCANNING, i, numsheets);
            if((j = sprites_add(i, sheets[i].name, sheets[i].w, sheets[i].h, 0, 0, i, 0, 0, sheets[i].w, sheets[i].h, 0, -1)) != -1) {
                sprites_colrow(i, &cols, &rows);
                sprites_split(j, cols, rows);
            }
        }
    }
}

/**
 * Add a new sprite with a single frame or a new frame to an existing sprite
 */
int sprites_add(int id, char *name, int w, int h, int dx, int dy, int sheet, int sx, int sy, int sw, int sh, int dur, int sfx)
{
    int i = numsprites, j;

    if(sheet < 0 || sheet >= numsheets || !sheets[sheet].data) {
        debug("error adding sprite, no such sprite sheet %u", sheet);
        return -1;
    }
    if(w < 1 || h < 1) {
        debug("invalid dimensions %u x %u for sprite id %u", w, h, id);
        return -1;
    }
    if(dx < 0) { w -= dx; dx = 0; }
    if(dy < 0) { h -= dy; dy = 0; }
    if(sx < 0) { sw += sx; sx = 0; }
    if(sy < 0) { sh += dy; sy = 0; }
    if(sx + sw > sheets[sheet].w) sw = sheets[sheet].w - sx;
    if(sy + sh > sheets[sheet].h) sh = sheets[sheet].h - sy;
    if(sprites) {
        if(id != -1) for(i = 0; i < numsprites && sprites[i].id != id; i++); else
        if(name && *name) for(i = 0; i < numsprites && strcmp(sprites[i].name, name); i++);
    }
    if(i == numsprites) {
        numsprites++;
        if((sprites = (sprite_t*)realloc(sprites, numsprites * sizeof(sprite_t)))) {
            engine_ram += sizeof(sprite_t);
            memset(&sprites[i], 0, sizeof(sprite_t));
            sprites[i].id = id < 0 ? i : id;
            if(name && *name) strncpy(sprites[i].name, name, NAMELEN - 1);
            sprites[i].w = w;
            sprites[i].h = h;
            if(sw > 0 && sh > 0) {
                w = 64; h = 64 * sh / sw;
                if(h > 64) { h = 64; w = 64 * sw / sh; }
                resize(sprites[i].preview, (64 - w) / 2, (64 - h) / 2, w, h, 64,
                    (sheets[sheet].data + (sy * sheets[sheet].w + sx)), sw, sh, sheets[sheet].w);
            }
        } else {
            engine_features |= EF_NOMEM;
            debug("memory allocation error, too many sprites %u", numsprites);
            numsprites = 0;
            return -1;
        }
    }
    if(sw > 0 && sh > 0) {
        j = sprites[i].numframe - 1;
        if(!sprites[i].numframe || sprites[i].frame[j].dx != dx || sprites[i].frame[j].dy != dy || sprites[i].frame[j].s != sheet ||
            sprites[i].frame[j].x != sx || sprites[i].frame[j].y != sy || sprites[i].frame[j].w != sw || sprites[i].frame[j].sfx != sfx) {
                j = sprites[i].numframe++;
                if((sprites[i].frame = (sprite_frame_t*)realloc(sprites[i].frame, sprites[i].numframe * sizeof(sprite_frame_t)))) {
                    engine_ram += sizeof(sprite_frame_t);
                    if(sprites[i].w < dx + sw) sprites[i].w = dx + sw;
                    if(sprites[i].h < dy + sh) sprites[i].h = dy + sh;
                    if(sprites[i].w < w) sprites[i].w = w;
                    if(sprites[i].h < h) sprites[i].h = h;
                    sprites[i].frame[j].dx = dx;
                    sprites[i].frame[j].dy = dy;
                    sprites[i].frame[j].s = sheet;
                    sprites[i].frame[j].x = sx;
                    sprites[i].frame[j].y = sy;
                    sprites[i].frame[j].w = sw;
                    sprites[i].frame[j].h = sh;
                    sprites[i].frame[j].dur = dur < 100 ? 100 : dur;
                    sprites[i].frame[j].sfx = sfx;
                    engine_features |= EF_SPRITES;
                } else {
                    engine_features |= EF_NOMEM;
                    debug("memory allocation error, too many frames for sprite #%u (id %u)", i, sprites[i].id);
                    sprites[i].numframe = 0;
                }
        } else
        if(sprites[i].numframe && dur) sprites[i].frame[j].dur += dur;
    } else {
        debug("invalid frame dimensions %u x %u for sprite #%u (id %u)", sw, sh, i, sprites[i].id);
    }
    return i;
}

/**
 * Draw an optionally scaled sprite frame into a pixel buffer
 */
void sprites_draw(int sprite, int frame, uint32_t *buf, int w, int h, int p, int x, int y, int scale)
{
    SDL_Surface *src, *dst;
    SDL_Rect srcrect, dstrect;
    sprite_frame_t *f;

    if(sprite < 0 || sprite >= numsprites || !buf || w < 1 || h < 1 || p < 1)
        return;
    if(scale < 1) scale = 1;
    if(!sprites[sprite].frame || frame < 0 || frame >= sprites[sprite].numframe || sprites[sprite].frame[frame].s < 0 ||
        sprites[sprite].frame[frame].s >= numsheets || !sheets[sprites[sprite].frame[frame].s].data) {
        src = SDL_CreateRGBSurfaceFrom(sprites[sprite].preview, 64, 64, 32, 256, 0xFF, 0xFF00, 0xFF0000, 0xFF000000);
        srcrect.x = 0; srcrect.y = 0; srcrect.w = 64; srcrect.h = 64;
        dstrect.x = x; dstrect.y = y; dstrect.w = 64 * scale; dstrect.h = 64 * scale;
    } else {
        f = &sprites[sprite].frame[frame];
        src = SDL_CreateRGBSurfaceFrom(sheets[f->s].data, sheets[f->s].w, sheets[f->s].h, 32, sheets[f->s].w * 4, 0xFF, 0xFF00, 0xFF0000, 0xFF000000);
        srcrect.x = f->x; srcrect.y = f->y; srcrect.w = f->w; srcrect.h = f->h;
        dstrect.x = x + f->dx * scale; dstrect.y = y + f->dy * scale; dstrect.w = f->w * scale; dstrect.h = f->h * scale;
    }
    dst = SDL_CreateRGBSurfaceFrom(buf, w, h, 32, p * 4, 0xFF, 0xFF00, 0xFF0000, 0xFF000000);
    SDL_SetSurfaceBlendMode(src, SDL_BLENDMODE_BLEND);
    SDL_SetSurfaceBlendMode(dst, SDL_BLENDMODE_BLEND);
    SDL_BlitScaled(src, &srcrect, dst, &dstrect);
    SDL_FreeSurface(src); SDL_FreeSurface(dst);
}

/**
 * Update the texture with the next frame (animate current sprite)
 */
void sprites_update(void)
{
    static uint32_t next = 0;
    static int last = -1, march = 0;
    char tmp[32];
    int x, y, w, h, p;
    uint32_t *buf;

    if(cursprite < 0 || cursprite >= numsprites || !sprites[cursprite].frame) return;
    if(!sprite || sprrect.w != sprites[cursprite].w || sprrect.h != sprites[cursprite].h) {
        sprrect.w = sprites[cursprite].w; sprrect.h = sprites[cursprite].h;
        if(sprite) { SDL_DestroyTexture(sprite); sprite = NULL; }
        if((sprite = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, sprrect.w, sprrect.h))) {
            SDL_SetTextureBlendMode(sprite, SDL_BLENDMODE_BLEND);
            SDL_SetTextureScaleMode(sprite, smooth ? SDL_ScaleModeLinear : SDL_ScaleModeNearest);
        }
        curframe = last = -1;
    }
    if(curframe < 0 || ticks >= next) {
        curframe++;
        if(curframe >= sprites[cursprite].numframe) curframe = 0;
        next = ticks + (insplit ? 100 : sprites[cursprite].frame[curframe].dur);
        if(!insplit) audio_play(sprites[cursprite].frame[curframe].sfx);
        march++; march &= 15;
    }
    if(curframe != last || insplit) {
        last = curframe;
        if(sprite) {
            SDL_LockTexture(sprite, NULL, (void*)&buf, &p);
            if(buf) {
                memset(buf, 0, p * sprrect.h);
                sprites_draw(cursprite, curframe, buf, sprrect.w, sprrect.h, sprrect.w, 0, 0, 1);
                if(insplit) {
                    w = sprrect.w / splitcols; h = sprrect.h / splitrows;
                    if(w > 2 && h > 2) {
                        for(x = w; x < sprrect.w - 2; x += w)
                            for(y = 0; y < sprrect.h - 2; y++)
                                buf[y * sprrect.w + x] = buf[y * sprrect.w + x + 1] =
                                    (march + y + x / w) & 8 ? 0xffffffff : 0xff000000;
                        for(y = h; y < sprrect.h - 2; y += h)
                            for(x = 0; x < sprrect.w - 2; x++)
                                buf[y * sprrect.w + x] = buf[(y + 1) * sprrect.w + x] =
                                    (march + x + y / h) & 8 ? 0xffffffff : 0xff000000;
                    }
                } else
                if(!screen_d) {
                    SDL_LockTexture(screen, NULL, (void*)&screen_d, &screen_p);
                    screen_p >>= 2;
                    sprintf(tmp, "%3d /%3d", curframe + 1, sprites[cursprite].numframe);
                    box(frmpos, screen_h - 22, 64, 16, BG);
                    text(frmpos, screen_h - 22, FG, tmp);
                    SDL_UnlockTexture(screen);
                    screen_d = NULL; screen_p = 0;
                }
            }
            SDL_UnlockTexture(sprite);
        }
    }
}

/**
 * Select current sprite to display
 */
void sprites_select(int idx)
{
    if(idx >= numsprites) idx = numsprites - 1;
    if(idx < 0) idx = 0;
    cursprite = idx;
    if(scrcur > cursprite * 68) scrcur = cursprite * 68;
    if(scrcur < (cursprite + 1) * 68 - screen_h) scrcur = (cursprite + 1) * 68 - screen_h;
    sprrect.w = sprrect.h = insplit = 0;
    sprites_update();
    audio_stop();
    refresh = 1;
}

/**
 * UI Controller
 */
void sprites_ctrl(void)
{
    uint8_t *buf;
    uint64_t size;
    char tmp[NAMELEN + 5], *s;
    int i;

    switch(event.type) {
        case SDL_KEYDOWN:
            switch(event.key.keysym.sym) {
                case SDLK_LEFT: pressedfield = 3; break;
                case SDLK_RIGHT: pressedfield = 4; break;
                case SDLK_UP: if(!insplit) sprites_select(cursprite - 1); else pressedfield = 5; break;
                case SDLK_DOWN: if(!insplit) sprites_select(cursprite + 1); else pressedfield = 6; break;
                case SDLK_HOME: sprites_select(0); break;
                case SDLK_END: sprites_select(numsprites - 1); break;
                case SDLK_PAGEUP: sprites_select(cursprite - (screen_h / 68 - 1)); break;
                case SDLK_PAGEDOWN: sprites_select(cursprite + (screen_h / 68 - 1)); break;
                case SDLK_RETURN: field = pressedfield = 0; break;
                case SDLK_SPACE: goto dosmooth;
                case SDLK_BACKSPACE: sprites_unsplit(cursprite); sprites_select(cursprite); break;
            }
        break;
        case SDL_KEYUP:
            switch(event.key.keysym.sym) {
                case SDLK_RETURN: field = pressedfield = 0; goto doexp;
                case SDLK_LEFT: if(insplit) { field = pressedfield = 3; goto doexp; } break;
                case SDLK_RIGHT: if(insplit) { field = pressedfield = 4; goto doexp; } break;
                case SDLK_UP: if(insplit) { field = pressedfield = 5; goto doexp; } break;
                case SDLK_DOWN: if(insplit) { field = pressedfield = 6; goto doexp; } break;
            }
        break;
        case SDL_MOUSEBUTTONUP:
doexp:      if(field == 0 && pressedfield == 0) {
                if(insplit) {
                    sprites_split(cursprite, splitcols, splitrows);
                    sprites_select(cursprite);
                } else
                if((buf = sprites_encode(cursprite, &size))) {
                    if(sprites[cursprite].name[0]) {
                        if((s = strrchr(sprites[cursprite].name, '/'))) s++; else
                        if((s = strrchr(sprites[cursprite].name, '\\'))) s++;
                        else s = sprites[cursprite].name;
                        strcpy(tmp, s);
                        if(!(s = strrchr(tmp, '.'))) s = tmp + strlen(tmp);
                    } else
                        s = tmp + sprintf(tmp, "sprite%05u", cursprite);
                    strcpy(s, sprites[cursprite].numframe > 1 ? ".gif" : ".png");
                    savedialog(sprites[cursprite].frame && sprites[cursprite].frame[0].s >= 0 && sprites[cursprite].frame[0].s < numsheets ?
                        sheets[sprites[cursprite].frame[0].s].path : NULL, tmp, buf, size);
                    free(buf);
                }
            } else
            if(field == 1 && pressedfield == 1) {
                if(sprites[cursprite].frame)
                    sprites_colrow(sprites[cursprite].frame[0].s, &splitcols, &splitrows);
                refresh = insplit = 1;
            } else
            if(field == 3 && pressedfield == 3 && splitcols > 1) { splitcols--; refresh = 1; sprites_update(); } else
            if(field == 4 && pressedfield == 4 && splitcols < 32) { splitcols++; refresh = 1; sprites_update(); } else
            if(field == 5 && pressedfield == 5 && splitrows > 1) { splitrows--; refresh = 1; sprites_update(); } else
            if(field == 6 && pressedfield == 6 && splitrows < 32) { splitrows++; refresh = 1; sprites_update(); } else
            if(field == 7 && pressedfield == 7) { sprrect.w = sprrect.h = insplit = 0; sprites_update(); }
        break;
        case SDL_MOUSEBUTTONDOWN:
            if(field == 2 || (mx < screen_w - 78 && my < screen_h - 24)) {
dosmooth:       smooth ^= 1; refresh = 1;
                if(sprite) SDL_SetTextureScaleMode(sprite, smooth ? SDL_ScaleModeLinear : SDL_ScaleModeNearest);
            } else
            if(mx >= screen_w - 78) {
                i = (scrcur + my - 38) / 68;
                if(SDL_GetModState())
                    sprites_merge(cursprite, i);
                else
                    sprites_select(i);
            }
        break;
    }
}

/**
 * UI View
 */
void sprites_view(void)
{
    char tmp[32];
    int i, j, tot = numsprites * 68 - (screen_h - 4);

    /* failsafes */
    if(!numsprites) {
        engine_features &= ~EF_SPRITES;
        tab = TAB_INFO;
        refresh = 1;
        return;
    }
    if(cursprite < 0) sprites_select(0);
    if(cursprite >= numsprites) sprites_select(numsprites - 1);

    if(!insplit) {
        i = textlen(lang[EXPORT]) + 16;
        fields[0].x = 2; fields[0].y = screen_h - 23; fields[0].w = i; fields[0].h = 21;
        button(fields[0].x, fields[0].y, i, 21, pressedfield == 0, lang[EXPORT]);
        j = frmpos = i + 6;
        i = textlen(lang[ANIMATE]) + 16;
        if(i < 64) i = 64;
        if(sprites[cursprite].numframe == 1) {
            fields[1].x = j; fields[1].y = screen_h - 23; fields[1].w = i; fields[1].h = 21;
            button(fields[1].x, fields[1].y, i, 21, pressedfield == 1, lang[ANIMATE]);
        } else {
            sprintf(tmp, "%3d /%3d", curframe + 1, sprites[cursprite].numframe);
            text(frmpos, screen_h - 22, FG, tmp);
        }
        j += i + 4;
        fields[2].x = j; fields[2].y = screen_h - 22; fields[2].w = 16; fields[2].h = 21;
        chkbox(fields[2].x, fields[2].y, smooth, "");
        if(sprites[cursprite].name[0]) {
            crop_w = screen_w - 12;
            text(j + 20, screen_h - 21, FG, sprites[cursprite].name);
            crop_w = screen_w;
        }
    } else {
        i = text(2, screen_h - 23, FG, lang[COLUMNS]);
        fields[3].x = 4 + i; fields[3].y = screen_h - 23; fields[3].w = fields[3].h = 21;
        button(fields[3].x, fields[3].y, 21, 21, pressedfield == 3, "⏴");
        i += 4 + 21;
        sprintf(tmp, "%3u", splitcols);
        txtfld(i, screen_h - 23, 26, 21, tmp);
        i += 26;
        fields[4].x = i; fields[4].y = screen_h - 23; fields[4].w = fields[4].h = 21;
        button(fields[4].x, fields[4].y, 21, 21, pressedfield == 4, "⏵");
        i = text(i + 37, screen_h - 23, FG, lang[ROWS]);
        fields[5].x = 4 + i; fields[5].y = screen_h - 23; fields[5].w = fields[5].h = 21;
        button(fields[5].x, fields[5].y, 21, 21, pressedfield == 5, "⏶");
        i += 4 + 21;
        sprintf(tmp, "%3u", splitrows);
        txtfld(i, screen_h - 23, 26, 21, tmp);
        i += 26;
        fields[6].x = i; fields[6].y = screen_h - 23; fields[6].w = fields[6].h = 21;
        button(fields[6].x, fields[6].y, 21, 21, pressedfield == 6, "⏷");
        fields[0].x = i + 37;
        i = textlen(lang[ANIMATE]) + 16;
        fields[0].y = screen_h - 23; fields[0].w = i; fields[0].h = 21;
        button(fields[0].x, fields[0].y, i, 21, pressedfield == 0, lang[ANIMATE]);
        fields[7].x = i + fields[0].x + 4;
        i = textlen(lang[CANCEL]) + 16;
        fields[7].y = screen_h - 23; fields[7].w = i; fields[7].h = 21;
        button(fields[7].x, fields[7].y, i, 21, pressedfield == 7, lang[CANCEL]);
    }

    hscroll(screen_w - 10, 0, screen_h, scrcur, tot, pressedfield == 999);

    crop_h = screen_h - 2;
    for(i = scrcur / 68; i < numsprites && i * 68 - scrcur < screen_h; i++) {
        box(screen_w - 78, 2 + i * 68 - scrcur, 66, 66, i == cursprite ? FG : BG - 0x080808);
        sprites_draw(i, -1, screen_d, screen_w, screen_h, screen_p, screen_w - 76, 3 + i * 68 - scrcur, 1);
    }

}
